﻿/*
Copyright (c) 2005-2008, CellAO Team

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the CellAO Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Collections.Generic;
using System.Text;
using AO.Core;
using Cell.Core;

namespace ZoneEngine
{
    /// <summary>
    /// 
    /// </summary>
    public class Character : CharacterBase
    {
        /// <summary>
        /// 
        /// </summary>
        public Client client;
        /// <summary>
        /// 
        /// </summary>
        public bool needpurge = true;

        #region Constructor / Destructor

        /// <summary>
        /// 
        /// </summary>
        /// <param name="characterId"></param>
        /// <param name="client"></param>
        public Character(int characterId, Client client)
            : base(characterId)
        {
            this.client = client;

            stats = new CharacterStats();

            if (characterId != 0)
            {
                ReadCoordinates();
                ReadStats();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        ~Character()
        {
            Purge();
        }

        #endregion

        /// <summary>
        /// 
        /// </summary>
        public void Purge()
        {
            if ((characterId != 0) && (needpurge))
            {
                needpurge = false;

                WriteCoordinates();
                WriteStats();
            }
        }

        #region Coordinates

        /// <summary>
        /// 
        /// </summary>
        public AOCoord coord
        {
            get
            {
                if ((moveDirection == MoveDirection.None) && (strafeDirection == SpinOrStrafeDirection.None))
                {
                    return rawCoord;
                }
                else if (spinDirection == SpinOrStrafeDirection.None)
                {
                    Vector3 moveVector = calculateMoveVector();

                    moveVector = moveVector * predictionDuration.TotalSeconds;

                    return new AOCoord(rawCoord.coordinate + moveVector);
                }
                else
                {
                    Vector3 moveVector;
                    Vector3 positionFromCentreOfTurningCircle;
                    double turnArcAngle;
                    double y;
                    double duration;

                    duration = predictionDuration.TotalSeconds;

                    moveVector = calculateMoveVector();
                    turnArcAngle = calculateTurnArcAngle();

                    // This is calculated seperately as height is unaffected by turning
                    y = rawCoord.coordinate.y + (moveVector.y * duration);

                    if (spinDirection == SpinOrStrafeDirection.Left)
                        positionFromCentreOfTurningCircle = new Vector3(moveVector.z, y, -moveVector.x);
                    else
                        positionFromCentreOfTurningCircle = new Vector3(-moveVector.z, y, moveVector.x);

                    return new AOCoord(rawCoord.coordinate + Quaternion.RotateVector3(new Quaternion(Vector3.AxisY, turnArcAngle), positionFromCentreOfTurningCircle) - positionFromCentreOfTurningCircle);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public Quaternion heading
        {
            get
            {
                if (spinDirection == SpinOrStrafeDirection.None)
                    return rawHeading;
                else
                {
                    double turnArcAngle;
                    Quaternion turnQuaterion;
                    Quaternion newHeading;

                    turnArcAngle = calculateTurnArcAngle();
                    turnQuaterion = new Quaternion(Vector3.AxisY, turnArcAngle);

                    newHeading = Quaternion.Hamilton(turnQuaterion, rawHeading);
                    newHeading.Normalize();

                    return newHeading;
                }
            }
        }

        /*
         * Do not use rawCoord and rawHeading in most cases you want the non-raw versions (which have interpolation)
         * The few cases you will need to do it is if you want to set where the client is (ie, teleporting, or when
         * we parse a CharDCMove packet) or when doing Interpolation calculations. So if you're not doing either of
         * those, dont use rawCoord and rawHeading :)
         */
        public AOCoord rawCoord;
        public Quaternion rawHeading = new Quaternion(0, 0, 0, 1);

        private DateTime predictionTime;
        public TimeSpan predictionDuration
        {
            get
            {
                DateTime currentTime = DateTime.Now;

                return currentTime - predictionTime;
            }
        }
        public int pf;

        public enum SpinOrStrafeDirection
        {
            None,
            Left,
            Right
        }
        public enum MoveDirection
        {
            None,
            Forwards,
            Backwards
        }
        public enum MoveMode
        {
            None,
            Rooted,
            Walk,
            Run,
            Swim,
            Crawl,
            Sneak,
            Fly,
            Sit,
            SocialTemp, // NV: What is this again exactly?
            Nothing,
            Sleep,
            Lounge
        }

        public SpinOrStrafeDirection spinDirection = SpinOrStrafeDirection.None;
        public SpinOrStrafeDirection strafeDirection = SpinOrStrafeDirection.None;
        public MoveDirection moveDirection = MoveDirection.None;
        public MoveMode moveMode = MoveMode.Run; // Run should be an appropriate default for now
        public MoveMode prevMoveMode = MoveMode.Run; // Run should be an appropriate default for now

        /// <summary>
        /// 
        /// </summary>
        public void stopMovement()
        {
            // This should be used to stop the interpolating and save last interpolated value of movement before teleporting
            rawCoord = coord;
            rawHeading = heading;

            spinDirection = SpinOrStrafeDirection.None;
            strafeDirection = SpinOrStrafeDirection.None;
            moveDirection = MoveDirection.None;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="moveType"></param>
        public void updateMoveType(byte moveType)
        {
            predictionTime = DateTime.Now;

            /*
             * NV: Would be nice to have all other possible values filled out for this at some point... *Looks at Suiv*
             * More specifically, 10, 13 and 22 - 10 and 13 seem to be tied to spinning with mouse. 22 seems random (ping mabe?)
             * Also TODO: Tie this with CurrentMovementMode stat and persistance (ie, log off walking, log back on and still walking)
             * Values of CurrentMovementMode and their effects:
                0: slow moving feet not animating
                1: rooted cant sit
                2: walk
                3: run
                4: swim
                5: crawl
                6: sneak
                7: flying
                8: sitting
                9: rooted can sit
                10: same as 0
                11: sleeping
                12: lounging
                13: same as 0
                14: same as 0
                15: same as 0
                16: same as 0
             */
            switch (moveType)
            {
                case 1: // Start Move Forwards
                    moveDirection = MoveDirection.Forwards;
                    break;
                case 2: // Stop Move Forwards
                    moveDirection = MoveDirection.None;
                    break;

                case 3: // Start Move Backwards
                    moveDirection = MoveDirection.Backwards;
                    break;
                case 4: // Stop Move Backwards
                    moveDirection = MoveDirection.None;
                    break;

                case 5: // Start Sidestep Right
                    strafeDirection = SpinOrStrafeDirection.Right;
                    break;
                case 6: // Stop Sidestep Right
                    strafeDirection = SpinOrStrafeDirection.None;
                    break;

                case 7: // Start Sidestep Left
                    strafeDirection = SpinOrStrafeDirection.Left;
                    break;
                case 8: // Stop Sidestep Left
                    strafeDirection = SpinOrStrafeDirection.None;
                    break;

                case 9: // Start Spin Right
                    spinDirection = SpinOrStrafeDirection.Right;
                    break;
                //case 10: // ??
                //    break;
                case 11: // Stop Spin Right
                    spinDirection = SpinOrStrafeDirection.None;
                    break;

                case 12: // Start Spin Left
                    spinDirection = SpinOrStrafeDirection.Left;
                    break;
                //case 13: // ??
                //    break;
                case 14: // Stop Spin Left
                    spinDirection = SpinOrStrafeDirection.None;
                    break;

                case 15: // Jump
                    // NV: TODO: This!
                    break;

                case 24: // Walk
                    moveMode = MoveMode.Walk;
                    break;
                case 25: // Run
                    moveMode = MoveMode.Run;
                    break;

                case 27: // Start Crawling
                    prevMoveMode = moveMode;
                    moveMode = MoveMode.Crawl;
                    break;
                case 40: // Stop Crawling
                    moveMode = prevMoveMode;
                    break;

                case 28: // Start Sneaking
                    prevMoveMode = moveMode;
                    moveMode = MoveMode.Sneak;
                    break;
                case 36: // Stop Sneaking
                    moveMode = prevMoveMode;
                    break;

                default:
                    //Console.WriteLine("Unknown MoveType: " + moveType);
                    break;
            }

            //Console.WriteLine((moveDirection != 0 ? moveMode.ToString() : "Stand") + "ing in the direction " + moveDirection.ToString() + (spinDirection != 0 ? " while spinning " + spinDirection.ToString() : "") + (strafeDirection != 0 ? " and strafing " + strafeDirection.ToString() : ""));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private bool canMove()
        {
            if ((moveMode == MoveMode.Run) || (moveMode == MoveMode.Walk) || (moveMode == MoveMode.Swim) || (moveMode == MoveMode.Crawl) || (moveMode == MoveMode.Sneak) || (moveMode == MoveMode.Fly))
                return true;

            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private int calculateEffectiveRunSpeed()
        {
            int effectiveRunSpeed;

            switch (moveMode)
            {
                case MoveMode.Run:
                    effectiveRunSpeed = stats.GetStat(156); // Stat #156 = RunSpeed
                    break;

                case MoveMode.Walk:
                    effectiveRunSpeed = -500;
                    break;

                case MoveMode.Swim:
                    // Swim speed is calculated the same as Run Speed except is half as effective
                    effectiveRunSpeed = stats.GetStat(138) / 2; // Stat #138 = Swim
                    break;

                case MoveMode.Crawl:
                    effectiveRunSpeed = -600;
                    break;

                case MoveMode.Sneak:
                    effectiveRunSpeed = -500;
                    break;

                case MoveMode.Fly:
                    effectiveRunSpeed = 2200; // NV: TODO: Propper calc for this!
                    break;

                default:
                    // All other movement modes, sitting, sleeping, lounging, rooted, etc have a speed of 0
                    // As there is no way to 'force' that this way, we just default to 0 and hope that canMove() has been called to properly check.
                    effectiveRunSpeed = 0;
                    break;
            }

            return effectiveRunSpeed;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private double calculateForwardSpeed()
        {
            double speed;
            int effectiveRunSpeed;

            if ((moveDirection == MoveDirection.None) || (!canMove()))
                return 0;

            effectiveRunSpeed = calculateEffectiveRunSpeed();

            if (moveDirection == MoveDirection.Forwards)
            {
                // NV: TODO: Verify this more. Especially with uber-low runspeeds (negative)
                speed = Math.Max(0, (effectiveRunSpeed * 0.005) + 4);

                if (moveMode != MoveMode.Swim)
                    speed = Math.Min(15, speed); // Forward speed is capped at 15 units/sec for non-swimming
            }
            else
            {
                // NV: TODO: Fix this, should be based off the above except nerfed a bit and negative
                speed = -Math.Max(0, (effectiveRunSpeed * 0.005) + 4);

                if (moveMode != MoveMode.Swim)
                    speed = Math.Max(-15, speed); // Backwards speed is capped at 15 units/sec for non-swimming
            }

            return speed;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private double calculateStrafeSpeed()
        {
            double speed;
            int effectiveRunSpeed;

            // Note, you can not strafe while swimming or crawling
            if ((strafeDirection == SpinOrStrafeDirection.None) || (moveMode == MoveMode.Swim) || (moveMode == MoveMode.Crawl) || (!canMove()))
                return 0;

            effectiveRunSpeed = calculateEffectiveRunSpeed();

            // NV: TODO: Update this based off Forward runspeed when that is checked (strafe effective run speed = effective run speed / 2)
            speed = ((effectiveRunSpeed / 2) * 0.005) + 4;

            if (strafeDirection == SpinOrStrafeDirection.Left)
                speed = -speed;

            return speed;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private double calculateMoveSpeed()
        {
            double forwardSpeed;
            double strafeSpeed;

            forwardSpeed = calculateForwardSpeed();
            strafeSpeed = calculateStrafeSpeed();

            // We use Pythagoras to determine the magnitude of the speed
            return Math.Sqrt(Math.Pow(forwardSpeed, 2) + Math.Pow(strafeSpeed, 2));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private Vector3 calculateMoveDirection()
        {
            Vector3 moveVector = calculateMoveVector();

            if (moveVector.Magnitude > 0)
                return calculateMoveVector().Normalize();
            else
                return Vector3.Origin;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private Vector3 calculateMoveVector()
        {
            double forwardSpeed;
            double strafeSpeed;
            Vector3 forwardMove;
            Vector3 strafeMove;

            if (!canMove())
                return Vector3.Origin;

            forwardSpeed = calculateForwardSpeed();
            strafeSpeed = calculateStrafeSpeed();

            if ((forwardSpeed == 0) && (strafeSpeed == 0))
                return Vector3.Origin;

            if (forwardSpeed != 0)
            {
                forwardMove = Quaternion.RotateVector3(rawHeading, Vector3.AxisZ);
                forwardMove.Magnitude = Math.Abs(forwardSpeed);
                if (forwardSpeed < 0)
                    forwardMove = -forwardMove;
            }
            else
            {
                forwardMove = Vector3.Origin;
            }

            if (strafeSpeed != 0)
            {
                strafeMove = Quaternion.RotateVector3(rawHeading, Vector3.AxisX);
                strafeMove.Magnitude = Math.Abs(strafeSpeed);
                if (strafeSpeed < 0)
                    strafeMove = -strafeMove;
            }
            else
            {
                strafeMove = Vector3.Origin;
            }

            return forwardMove + strafeMove;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private double calculateTurnTime()
        {
            int turnSpeed;
            double turnTime;

            turnSpeed = stats.GetStat(267); // Stat #267 TurnSpeed

            if (turnSpeed == 0)
                turnSpeed = 40000;

            turnTime = 70000 / turnSpeed;

            return turnTime;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private double calculateTurnArcRadius()
        {
            double turnTime;
            double moveSpeed;
            double radius;

            turnTime = calculateTurnTime();

            moveSpeed = calculateMoveSpeed();

            radius = turnTime * moveSpeed / (2 * Math.PI);

            return radius;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private double calculateTurnArcAngle()
        {
            double turnTime;
            double angle;
            double modifiedDuration;

            turnTime = calculateTurnTime();

            modifiedDuration = predictionDuration.TotalSeconds % turnTime;

            angle = 2 * Math.PI * modifiedDuration / turnTime;

            return angle;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public bool ReadCoordinates()
        {
            mySql.MySqlRead("SELECT `ZoneID`, `X`, `Y`, `Z`, `H1`, `H2`, `H3`, `H4` FROM `characters` WHERE ID = '" + characterId + "' LIMIT 1");

            if (!mySql.myreader.Read())
            {
                mySql.myreader.Close();

                mySql.mcc.Close();

                return false;
            }

            pf = mySql.myreader.GetInt32(0);
            rawCoord = new AOCoord(mySql.myreader.GetFloat(1), mySql.myreader.GetFloat(2), mySql.myreader.GetFloat(3));
            rawHeading = new Quaternion(mySql.myreader.GetFloat(4), mySql.myreader.GetFloat(5), mySql.myreader.GetFloat(6), mySql.myreader.GetFloat(7));

            mySql.myreader.Close();

            mySql.mcc.Close();

            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public bool WriteCoordinates()
        {
            mySql.MySqlUpdate(String.Format(System.Globalization.CultureInfo.InvariantCulture, "update `characters` SET `ZoneID` = '{0}', `X` = '{1}', `Y` = '{2}', `Z` = '{3}', `H1` = '{4}', `H2` = '{5}', `H3` = '{6}', `H4` = '{7}' WHERE `ID` = '{8}'",
                pf, coord.x, coord.y, coord.z, heading.xf, heading.yf, heading.zf, heading.wf, characterId));
            return true;
        }

        #endregion

        #region Stats

        /// <summary>
        /// 
        /// </summary>
        public CharacterStats stats;

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public bool WriteStats()
        {
            return stats.WriteStats(this);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public bool ReadStats()
        {
            return stats.ReadStats(this);
        }

        #endregion
    }
}
